Android 插件化技术 —— 从 VirtualAPK 开始

Android 插件化技术 —— 从 VirtualAPK 开始

前言

安卓的插件化技术在这一两年越来越流行,虽然这种行为被谷歌命令禁止,而且在版本控制上越来越严格(最新的 Android P 对非 SDK 做了非常大的限制,而这些接口通常是插件化方案必须调用的接口)但是由于其动态化的部署方案确实有着非常多的优点(模块解耦,动态配置,减少 APK 体积等),这篇文章将从滴滴开源的 VirtualAPK 来分析市场上主流的插件化 APP 实现方式。本文的代码基于 Android N,后续 Android 的代码或多或少地都会有一些改动,但是大致方向是不会变的。

强烈建议去看一遍 startActivity启动过程分析,只有清楚了启动流程才能明白一些关键点的代码是如何处理的

原理

Android 的插件化技术实现主要依靠基于 ClassLoader 的动态加载机制,但是在 Android 中四大组件的使用必须提前在 Manifest.xml 中注册,所以也必须在宿主中预埋一些基础四大组件,在实际运行中需要通过反射以及 hock 一些系统关键类去实现对应关系

类加载器及双亲委托机制

Java 的代码都是以 class 的形式存在的,当虚拟机运行时就需要一种机制将 class 内容加载到内存中才能创建我们需要的实例。而负责这种加载机制的就是类加载器 ClassLoader。

提到类加载器那就必须提到类加载器的双亲委托机制,Android 虚拟机和 Java 虚拟机虽然有些细微的差别,但是总体上的机制是一致的。

在 Android 中常用的类加载器包括 DexClassLoader 和 PathClassLoader,区别在于 PathClassLoader 只能加载系统已安装过的 Apk,而 DexClassLoader 可以加载自定义目录下的 Jar/Apk/Dex,常用于动态加载。而两者都继承自 BaseDexClassLoader,最终继承自 ClassLoader,会在构造函数中会传入 parent(在 Android 系统中就是 BootClassLoader,该加载器会加载一些系统级别的类),在加载类时 ClassLoader 会先由 parent 去加载,如果没有找到对应的类才会再交给自己去实现加载逻辑。

首先看 jdk 的类加载方法

ClassLoader.loadClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected Class<?> loadClass(String name, boolean resolve) 
throws ClassNotFoundException
{
// 首先会查找类是否已经被加载器加载过,所以被替换的类需要在原类未被加载之前替换
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 先由父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 未查找到会抛出该异常,所以需要捕获
}

if (c == null) {
// 如果父加载器查找不到类才会交由本身去加载
c = findClass(name);
}
}
return c;
}

在 Android sdk 中 BaseDexClassLoader 略有些不同

双亲委托机制优点:

  1. 类共享,被顶层类加载器加载过的类将会被缓存在内存中,任何子加载器加载该类都可以从内存中直接获取
  2. 类安全,一些系统级别的类只会被顶层加载器加载,这就避免了用自己的代码去替换核心类的一些恶意行为

插件框架

Activity 代理

startActivity启动过程分析 一文中可以详细的了解一个 activity 启动的全过程,这里提几个校验的关键点,Instrumentation 源码

  • intent 的构造函数中会生成 ComponentName 成员变量,保存当前的 packageName 以及目标 class 的 className
  • ActivityManagerService 在启动一个 activity 时会通过 ActivityStackSupervisor 的 resolveActivity 方法去 PackageManagerService 查找对应的 activity 信息(PackageManagerService 会解析 Manifest.xml 获取整个应用的信息),无法查找到在 startActivityLocked 方法中则会返回 ActivityManager.START_INTENT_NOT_RESOLVED 的 err 信息,最终返回到 Instrumentation 中抛出我们开发中常见的 ActivityNotFoundException 异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}

switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);

...
}
}

这种限制导致了必须在宿主 Apk 中的 Manifest.xml 中预埋足够多的宿主代理 Activity。
早期的插件化框架则是在这些宿主的 ProxyActivity 中运行时生成将生命周期方法代理插件 Activity 中,然而这种方法的缺点就是由于两者的上下文环境即 context 是不同的,为了兼容实际的使用需求必须要 hock 许许多多的方法去对应正确的 context,需要对基类做比较多的工作。

而 VirtualAPK 的做法则是 hock 一些关键节点更改信息来“欺骗” AMS,具体的做法就是从 Instrumentation 着手,该类是一个工具类,用于协助 ActivityThread 完成一些琐碎的工作,在 VirtualAPK 中通过 VAInstrumentation 去代理。

在 Activity 启动流程中,activity 的 startActivityForResult 方法会调用 Instrumentation.execStartActivity 方法,在这个方法里就可以改变 Intent 携带的信息

VAInstrumentation.injectIntent

1
2
3
4
5
6
7
8
9
10
protected void injectIntent(Intent intent) {
// 查找插件 activity 在插件包中注册的信息
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
// 如果 Component 为空则表示不在插件包,而是在宿主包中的
if (intent.getComponent() != null) {
Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
// 一些必要的信息替换工作
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
}

通过 ComponentsHandler.transformIntentToExplicitAsNeeded -> PluginManager.resolveActivity -> LoadedPlugin.resolveActivity -> LoadedPlugin.chooseBestActivity 查找到插件 activity 在插件包中注册的信息。

然后通过

ComponentsHandler.markIntentIfNeeded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}

String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// 如果是插件包,将原本的包名和类名存储在 intent 中
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
// 查找合适的代理信息
dispatchStubActivity(intent);
}
}

通过 ComponentsHandler.dispatchStubActivity -> StubActivityInfo.getStubActivity(String className, int launchMode, Theme theme) 根据启动模式和主题样式,去获取到合理的代理 activity 信息

就这样将插件中 activity 信息替换成代理信息,然后有 Instrumentation 交由后续的代码处理,待到 AMS 处理返回通过 ApplicationThread 经由 handler 传递 LAUNCH_ACTIVITY 的消息到 ActivityThread

VAInstrumentation 继承了 Handler.Callback 接口,早在初始化的 hock 阶段,PluginManager 的 hookInstrumentationAndHandler 方法中就为 ActivityThread 中的 mH 这个 handler 设置 VAInstrumentation 这个callback,那 VAInstrumentation 可以在 ActivityThread 之前收到启动请求

VAInstrumentation.handleMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
// ActivityClientRecord r,此时 activity 的实例并没有生产
Object r = msg.obj;
try {
Intent intent = (Intent) ReflectUtil.getField(r.getClass(), r, "intent");
// 这里替换了 classloader 防止加载 Parcelable 是因 classloader 不同导致的类型不同
intent.setExtrasClassLoader(VAInstrumentation.class.getClassLoader());
ActivityInfo activityInfo = (ActivityInfo) ReflectUtil.getField(r.getClass(), r, "activityInfo");

if (PluginUtil.isIntentFromPlugin(intent)) {
// 替换主题
int theme = PluginUtil.getTheme(mPluginManager.getHostContext(), intent);
if (theme != 0) {
Log.i(TAG, "resolve theme, current theme:" + activityInfo.theme + " after :0x" + Integer.toHexString(theme));
activityInfo.theme = theme;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 不会拦截后续的逻辑
return false;
}

在经由 ActivityThread,交由 Instrumentation.newActivity 生产 Activity 实例,VAInstrumentation 同样覆写了该方法

VAInstrumentation.newActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
// 由于宿主代理类并不会有实例,所有会抛出 ClassNotFoundException 异常
try {
cl.loadClass(className);
Log.i(TAG, String.format("newActivity[%s]", className));
} catch (ClassNotFoundException e) {
// 根据开始存入 intent 的插件类信息生成 component
ComponentName component = PluginUtil.getComponent(intent);

if (component == null) {
return newActivity(mBase.newActivity(cl, className, intent));
}

String targetClassName = component.getClassName();
Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));
// 根据包名获取到插件
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);

if (plugin == null) {
// 没有对应插件会在debug模式下报错,属于异常情况
boolean debuggable = false;
try {
Context context = this.mPluginManager.getHostContext();
debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
} catch (Throwable ex) {

}

if (debuggable) {
throw new ActivityNotFoundException("error intent: " + intent.toURI());
}

Log.i(TAG, "Not found. starting the stub activity: " + StubActivity.class);
return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent));
}

// 通过插件中的 classloader 加载,该 classloader 为 DexClassLoader,可以
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);

// for 4.1+
// 替换资源文件
Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());

return newActivity(activity);
}

return newActivity(mBase.newActivity(cl, className, intent));
}

待到 activity 生成以后就开始生命周期的回调了,ActivityThread 会调用 Instrumentation.callActivityOnCreate 方法触发 activity 的 onCreate,在这里 VAInstrumentation 会 hock 住替换掉 activity 中的一些相关资源类

VAInstrumentation.injectActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void injectActivity(Activity activity) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
Reflector.with(base).field("mResources").set(plugin.getResources());
Reflector reflector = Reflector.with(activity);
reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
reflector.field("mApplication").set(plugin.getApplication());

// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
} catch (Exception e) {
Log.w(TAG, e);
}
}
}

会一次替换掉,mResources:使用插件包中的资源,mBase:对 context 进行 hock,用于替换一些上下文环境,具体可以在 PluginContext 中查找,mApplication:替换成插件包中的 Application。 这样就完成了一个插件 activity 的欺骗启动了。

其他代理

至于其他的的基础组件代理,其实只要明白了启动流程,找到关键点进行替换也是同样的道理,只是具体的实现可能不同吧

Android P 的适配

首先在 ActivityThread 类中的 H handler 类,原先 ApplicationThread 会通过 LAUNCH_ACTIVITY(100) 去传递信息通知 ActivityThread 去初始化 Activity
。但是在 Android P 中,谷歌将所有关于 activity 生命周期的通知消息整合成了 EXECUTE_TRANSACTION(159),通过 TransactionExecutor 交由对应的 ClientTransactionItem.execute 方法执行,负责启动 Activity 的就是 LaunchActivityItem 了

Android P 引入了针对非 SDK 接口(俗称为隐藏API)的使用限制。这对于插件化技术来说无疑是一个非常重大的打击,毕竟插件化技术完全是基于反射替换系统关键类实现的

这篇文章将会深入探讨谷歌对于接口限制的工作原理,也提供了几种解决方案 一种绕过Android P上非SDK接口限制的简单方法

结语

插件化技术确实能有效提升开发以及发版的效率,但是适配问题一直是插件化的最头疼的问题,面对 Android 严重的碎片化以及谷歌对插件化技术的严厉禁止,插件化技术的未来还是有很长的路要走。